-
Notifications
You must be signed in to change notification settings - Fork 665
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce CardEditUIHandler for the CardEditUI #10462
base: master
Are you sure you want to change the base?
Conversation
Diffuse output:
APK
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should merge this in once it's actually being used, rather than checking in unused code. I think a natural way to merge in this initial change would be to just replace the existing card details UI with this new UI and its handler. So I would also expect that all the card brand choice changes are included in the initial PR, but probably not the "card details" parts (presumably those are the other fields that will be changed as part of the edit card project)
5a49c4e
to
918bf9a
Compare
4c0a385
to
f1389e5
Compare
paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/CardEditUIHandler.kt
Outdated
Show resolved
Hide resolved
* | ||
* @param cardBrandChoice The newly selected card brand choice | ||
*/ | ||
fun onBrandChoiceChanged(cardBrandChoice: CardBrandChoice) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should replace the callbacks in favor of a single method that handles view actions. A sealed class representing all the view actions can then just be appended to.
sealed interface ViewAction {
data class OnBrandChoiceChanged(val cardBrandChoice: CardBrandChoice) : ViewAction
data class OnCardDetailsChanged(val cardUpdateParams: CardUpdateParams?) : ViewAction
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fun onBrandChoiceChanged(cardBrandChoice: CardBrandChoice)
is a function meant to be called from the UI when a new brand is selected.val onBrandChoiceChanged: BrandChoiceCallback
is meant for analytics purposes. It's fired when the selected brand in the function above is different from the original. The brandChoiceChange function parameter is sent downstream from [Payment|Customer]SheetViewModel to UpdatePaymentMethodInteractor.EditCardDetailsInteractor
is handling CBC change now, that's why I added the function parameter
*val onCardDetailsChanged: CardDetailsCallback
is a callback that emits edit card parameters to a consumer (UpdatePaymentMethodInteractor
)
I don't think onCardDetailsChanged
fits into the viewAction because of the behaviour I described above. I could move the brandChanged function into a ViewAction interface. What do you think?
/** | ||
* Callback for when the card brand choice changes. | ||
*/ | ||
val onBrandChoiceChanged: BrandChoiceCallback |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this unintentional? I see two callbacks for handling changes to card brand choice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The one highlighted in this comments is to send an analytics event upstream. The other is for when the user selects a brand from the dropdown. I expanded more here
I could rename it to something with analytics in its name. What do you think?
|
||
internal typealias CardDetailsCallback = (CardUpdateParams?) -> Unit | ||
|
||
internal typealias BrandChoiceCallback = (CardBrand) -> Unit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels redundant since brand choice can be controlled from CardUpdateParams
as well. Are we going to combine these later? Could each individual field be a view action?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This callback is for analytics. I don't want the component using this interactor to have to determine when the cardbrand is changed. CardUpdateParams
has expiryDate and address values as well, so a new cardUpdateParam does not mean the card brand is changed. The consumer will need to add logic (trivial I admit) to determine if there's a new card brand.
internal data class CardUpdateParams(
val expiryMonth: Int? = null,
val expiryYear: Int? = null,
val cardBrand: CardBrand? = null,
val billingDetails: PaymentMethod.BillingDetails? = null,
)
} | ||
) | ||
|
||
assertThat(handler.selectedBrand).isEqualTo(CardBrand.CartesBancaires) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does it default to CartesBancaires
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CARD_WITH_NETWORKS
test fixture defaults to CartesBancaires
. It was probably an arbitrary choice when the fixture was created
private fun handler(
card: PaymentMethod.Card = PaymentMethodFixtures.CARD_WITH_NETWORKS,
cardBrandFilter: CardBrandFilter = DefaultCardBrandFilter,
var cardUpdateParams: CardUpdateParams? = null | ||
val handler = handler( | ||
onCardDetailsChanged = { | ||
cardUpdateParams = it | ||
} | ||
) | ||
|
||
assertThat(handler.selectedBrand).isEqualTo(CardBrand.CartesBancaires) | ||
|
||
handler.brandChanged(CardBrand.Visa) | ||
|
||
assertThat(cardUpdateParams).isEqualTo(cardUpdateParams(cardBrand = CardBrand.Visa)) | ||
|
||
handler.brandChanged(CardBrand.CartesBancaires) | ||
|
||
assertThat(cardUpdateParams).isNull() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
usually tests follow a pattern of:
- setup
- test
- assert
This is also known as: arrange, act, assert
in this case I suppose the test should look like this. Note that there is a single line between each section which acts as a separator between each section: arrange, act and assert.
@Test
fun xxx() {
var cardUpdateParams: CardUpdateParams? = null
val handler = handler(
onCardDetailsChanged = {
cardUpdateParams = it
}
)
handler.brandChanged(CardBrand.Visa)
assertThat(cardUpdateParams).isEqualTo(cardUpdateParams(cardBrand = CardBrand.Visa))
}
If we want to test initial states like defaulting to CardBrand.CartesBancaires
or cardUpdateParams
being null, we should keep that in a different test that is ONLY testing that.
/** | ||
* The card being edited. | ||
*/ | ||
val card: PaymentMethod.Card | ||
|
||
/** | ||
* Filter for determining which card brands are available. | ||
*/ | ||
val cardBrandFilter: CardBrandFilter | ||
|
||
/** | ||
* Icon resource ID for the payment method. | ||
*/ | ||
val paymentMethodIcon: Int | ||
|
||
/** | ||
* Whether to show the card brand dropdown. | ||
*/ | ||
val showCardBrandDropdown: Boolean | ||
|
||
/** | ||
* Current state of the card edit UI. | ||
*/ | ||
val state: StateFlow<State> | ||
|
||
/** | ||
* Callback for when the card brand choice changes. | ||
*/ | ||
val onBrandChoiceChanged: BrandChoiceCallback | ||
|
||
/** | ||
* Callback for when card details change. It provides the values needed to | ||
* update the card, if any. | ||
*/ | ||
val onCardDetailsChanged: CardDetailsCallback |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Imho, these comments hurt more than the help -- imho, i'd remove them because the variable names seem to speak for themselves, but i'll leave it up to you guys.
/** | ||
* Whether to show the card brand dropdown. | ||
*/ | ||
val showCardBrandDropdown: Boolean |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
imho, shouldShowCardBrandDropdown
or isCardBrandDropdownShown
are better names as this is a Boolean
|
||
internal typealias CardDetailsCallback = (CardUpdateParams?) -> Unit | ||
|
||
internal typealias BrandChoiceCallback = (CardBrand) -> Unit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/BrandChoiceCallback/CardBrandChoiceCallback/
or maybe
s/BrandChoiceCallback/CardBrandCallback/
import com.stripe.android.paymentsheet.CardUpdateParams | ||
import kotlinx.coroutines.flow.StateFlow | ||
|
||
internal typealias CardDetailsCallback = (CardUpdateParams?) -> Unit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling this "CardDetailsCallback" introduces "Details" as new nomenclature. It seems like we avoid this by just changing the name of this to: CardUpdateParamsCallback
onCardDetailsChanged: CardUpdateParamsCallback | ||
): EditCardDetailsInteractor | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A lot of the exposed attributes here are implementation heavy and we should remove what's necessary. Ideally the interactor only exposes what is important to the UI which is:
- an observable state
- a set of view actions
The rest of these attributes should be implementation detail specific to the default implementation of the interactor. These can be managed as private parameters on the default implementation:
- The card being edited.
- The card brand filter.
- payment method icon
- analytics callbacks for card details changing and card brand choice.
See the SelectSavedPaymentMethodsInteractor as an example. The default create
method exposed via the interactor can also just create the default implementation.
) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use the interactor within CardDetailsUI
and UpdatePaymentMethodInteractor
in this PR.
* | ||
* @param cardBrandChoice The newly selected card brand choice | ||
*/ | ||
fun onBrandChoiceChanged(cardBrandChoice: CardBrandChoice) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There will be more view actions for handling other card edit changes? When we do add those actions, let's create a ViewAction
type with all the possible actions that the interactor can handle then expose a single method called handleViewAction
that the UI can use.
Doesn't need to happen in this PR!
Update EditCardDetailsInteractor.kt Remove callback from interface rename factory Update UpdatePaymentMethodInteractor.kt Update UpdatePaymentMethodInteractor.kt Update FakeEditCardDetailsInteractor.kt
9a16640
to
c2985ed
Compare
Summary
This will hold all the state for the content on the CardDetails UI. It will also emit changes to
CardUpdateParams
to theUpdatePaymentMethodInteractor
.CardUpdateParams
is the model that holds all the properties that have changed.It only supports CBC changes at the moment.
Motivation
Breaking up this PR
JIRA
https://docs.google.com/document/d/1mvmApRTfsmQd0A4-b6g8EGuerlF0VE6N2eHr1F19HSk/edit?tab=t.0#heading=h.gl2i202sfdwi
Testing
Changelog